{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "dce17d71-57b0-43c0-b92e-0154e0974f81",
   "metadata": {},
   "source": [
    "# 提取 - 使用聊天模型和少量示例从文本和其他非结构化媒体中提取结构化数据。\n",
    "\n",
    "### 构建一个信息提取链\n",
    "\n",
    "我们将使用聊天模型的 **工具调用（tool-calling）** 功能，从非结构化文本中提取结构化信息。我们还将演示如何在这种场景下使用 **少样本提示（few-shot prompting）** 来提升模型的表现效果。\n",
    "\n",
    "### 模式（Schema）\n",
    "\n",
    "首先，我们需要描述我们希望从文本中提取哪些信息。\n",
    "\n",
    "我们将使用 Pydantic 来定义一个示例模式，用于提取个人信息。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "4989f071-9321-44bb-a1da-0feb44f47960",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Optional\n",
    "\n",
    "from pydantic import BaseModel, Field\n",
    "\n",
    "\n",
    "class Person(BaseModel):\n",
    "    \"\"\"Information about a person.\"\"\"\n",
    "\n",
    "    # ^ Doc-string for the entity Person.\n",
    "    # This doc-string is sent to the LLM as the description of the schema Person,\n",
    "    # and it can help to improve extraction results.\n",
    "\n",
    "    # Note that:\n",
    "    # 1. Each field is an `optional` -- this allows the model to decline to extract it!\n",
    "    # 2. Each field has a `description` -- this description is used by the LLM.\n",
    "    # Having a good description can help improve extraction results.\n",
    "    name: Optional[str] = Field(default=None, description=\"该人的姓名\")\n",
    "    hair_color: Optional[str] = Field(\n",
    "        default=None, description=\"如果知道的话，请告知头发的颜色\"\n",
    "    )\n",
    "    height_in_meters: Optional[str] = Field(\n",
    "        default=None, description=\"高度以米为单位测量\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "325ab46d-d03c-406b-b557-729f6c79691f",
   "metadata": {},
   "source": [
    "在定义模式（schema）时，有两个最佳实践：\n",
    "\n",
    "1. **对属性和整个模式进行详细描述**：这些信息会被发送给大语言模型（LLM），有助于提升信息提取的准确性。\n",
    "2. **不要强制模型编造信息！** 如上所示，我们使用了 `Optional` 来允许模型在不知道答案时返回 `None`，而不是强行输出不存在的信息。\n",
    "\n",
    "🔴 **重要提示**  \n",
    "为了获得最佳效果，请对模式进行清晰的文档说明，并确保在文本中没有相关信息时，不要强制模型返回结果。\n",
    "\n",
    "### 信息提取器（The Extractor）\n",
    "\n",
    "让我们使用上面定义的模式，创建一个信息提取器。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "4cf884a9-9e7d-4003-98ac-f899bb8821b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "\n",
    "prompt_template = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        # 你是一个专业信息提取算法。\n",
    "        # 只需从文本中提取相关信息。\n",
    "        # 如果你不知道某个需要提取属性的值，\n",
    "        # 请将该属性的值返回为 null。\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are an expert extraction algorithm. \"\n",
    "            \"Only extract relevant information from the text. \"\n",
    "            \"If you do not know the value of an attribute asked to extract, \"\n",
    "            \"return null for the attribute's value.\",\n",
    "        ),\n",
    "        # Please see the how-to about improving performance with\n",
    "        # reference examples.\n",
    "        # MessagesPlaceholder('examples'),\n",
    "        (\"human\", \"{text}\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "aad6433f-acf1-4116-8db2-c26a2728db56",
   "metadata": {},
   "outputs": [],
   "source": [
    "import getpass\n",
    "import os\n",
    "\n",
    "try:\n",
    "    # load environment variables from .env file (requires `python-dotenv`)\n",
    "    from dotenv import load_dotenv\n",
    "\n",
    "    _ = load_dotenv()\n",
    "except ImportError:\n",
    "    pass\n",
    "\n",
    "if not os.environ.get(\"DASHSCOPE_API_KEY\"):\n",
    "  os.environ[\"DASHSCOPE_API_KEY\"] = getpass.getpass(\"Enter API key for OpenAI: \")\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "d6d1acdf-acfa-4bd9-9f51-787e5737b3c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.chat_models.tongyi import ChatTongyi\n",
    "\n",
    "llm = ChatTongyi(\n",
    "    streaming=True,\n",
    "    name=\"qwen-vl-max\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "422ed817-db34-418a-84cb-26711222742c",
   "metadata": {},
   "outputs": [],
   "source": [
    "structured_llm = llm.with_structured_output(schema=Person)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "475940a4-7952-4f6b-a39d-8500da7a6531",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Person(name='拉姿丽·艾玛尔', hair_color='金发', height_in_meters='1.65')"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "text = \"七龙珠中拉姿丽·艾玛尔，性别女，身高大约165厘米，金发，种族是改造人\"\n",
    "prompt = prompt_template.invoke({\"text\": text})\n",
    "structured_llm.invoke(prompt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "36f3cdee-2fde-4af3-bcf0-7ac4bb4f239e",
   "metadata": {},
   "source": [
    "### 多个实体（Multiple Entities）\n",
    "\n",
    "在大多数情况下，你可能需要提取的是一**组实体**（entity list），而不是单个实体。\n",
    "\n",
    "使用 Pydantic，你可以通过将模型相互嵌套的方式来轻松实现这一点。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "ff13afec-4670-465b-9de4-a8d438550df1",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import List, Optional\n",
    "\n",
    "from pydantic import BaseModel, Field\n",
    "\n",
    "\n",
    "class Person(BaseModel):\n",
    "    \"\"\"Information about a person.\"\"\"\n",
    "\n",
    "    # 这是实体 \"Person\" 的文档字符串（doc-string）。\n",
    "    # 这个文档字符串会被发送给大语言模型（LLM），作为对 \"Person\" 模型的描述，\n",
    "    # 它有助于提升信息提取的准确性。\n",
    "    \n",
    "    # 注意：\n",
    "    # 1. 每个字段都是 `Optional` 类型 —— 这样允许模型在无法识别时选择不提取该字段！\n",
    "    # 2. 每个字段都有一个 `description` 描述 —— LLM 会使用这些描述来理解字段含义。\n",
    "    # 提供清晰准确的描述可以显著提升提取效果。\n",
    "    name: str = Field(...,description=\"该人的姓名\")\n",
    "    hair_color: Optional[str] = Field(\n",
    "        default=None, description=\"如果知道的话，请告知头发的颜色\"\n",
    "    )\n",
    "    height_in_meters: Optional[str] = Field(\n",
    "        default=None, description=\"高度以米为单位测量\"\n",
    "    )\n",
    "\n",
    "\n",
    "class Data(BaseModel):\n",
    "    \"\"\"Extracted data about people.\"\"\"\n",
    "\n",
    "    # Creates a model so that we can extract multiple entities.\n",
    "    people: List[Person]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "6b4f01c8-1798-473f-a29d-c42351e40fca",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Data(people=[])"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "structured_llm = llm.with_structured_output(schema=Data)\n",
    "text = \"七龙珠中拉姿丽·艾玛尔，性别女，身高大约165厘米，金发，种族是改造人。还有孙悟空是主角，写别男\"\n",
    "prompt = prompt_template.invoke({\"text\": text})\n",
    "res = structured_llm.invoke(prompt)\n",
    "res"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20feeea9-9e54-47b6-b049-77544b1fa596",
   "metadata": {},
   "source": [
    "### 参考示例（Reference Examples）\n",
    "\n",
    "可以通过**少样本提示**（few-shot prompting）来引导大语言模型（LLM）应用的行为。对于聊天模型来说，这可以表现为一系列**输入与响应消息的配对**，用以展示期望的行为。\n",
    "\n",
    "例如，我们可以通过交替的用户和助手消息来传达某个符号的含义："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "582e88c7-f69b-4a95-a139-8e562b9b2804",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "7\n"
     ]
    }
   ],
   "source": [
    "messages = [\n",
    "    {\"role\": \"user\", \"content\": \"2 🦜 2\"},\n",
    "    {\"role\": \"assistant\", \"content\": \"4\"},\n",
    "    {\"role\": \"user\", \"content\": \"2 🦜 3\"},\n",
    "    {\"role\": \"assistant\", \"content\": \"5\"},\n",
    "    {\"role\": \"user\", \"content\": \"3 🦜 4\"},\n",
    "]\n",
    "\n",
    "response = llm.invoke(messages)\n",
    "print(response.content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "79da937b-fbf3-46b6-bfb6-53bbae223d15",
   "metadata": {},
   "source": [
    "结构化输出通常在底层使用 **工具调用（tool calling）** 机制。这通常包括生成包含工具调用的 AI 消息，以及包含工具调用结果的工具消息。\n",
    "\n",
    "在这种情况下，一组消息的顺序应该是什么样的？\n",
    "\n",
    "不同的聊天模型提供商对有效的消息序列有不同的要求。有些接受如下形式的（可重复的）消息序列：\n",
    "\n",
    "- 用户消息  \n",
    "- 包含工具调用的 AI 消息  \n",
    "- 包含工具调用结果的工具消息  \n",
    "\n",
    "而另一些则还要求一个最终的 AI 消息，其中包含某种形式的响应。\n",
    "\n",
    "LangChain 提供了一个实用函数 `tool_example_to_messages`，它可以为大多数模型提供商生成一个合法的消息序列。它通过只需要提供相应的 Pydantic 工具调用表示，简化了结构化少样本示例的生成过程。\n",
    "\n",
    "我们来试一下这个方法。我们可以将输入字符串和期望的 Pydantic 对象配对，转换为可以提供给聊天模型的一组消息。在底层，LangChain 会将这些工具调用格式化为各个模型提供商所需的格式。\n",
    "\n",
    "> 注意：此版本的 `tool_example_to_messages` 要求 `langchain-core>=0.3.20`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "a0ada1ca-c939-4b11-9c48-e696cbef749b",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\asus\\AppData\\Local\\Temp\\ipykernel_21340\\4279074080.py:23: LangChainBetaWarning: The function `tool_example_to_messages` is in beta. It is actively being worked on, so the API may change.\n",
      "  messages.extend(tool_example_to_messages(txt, [tool_call], ai_response=ai_response))\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.utils.function_calling import tool_example_to_messages\n",
    "\n",
    "examples = [\n",
    "    (\n",
    "        \"海洋广阔而蔚蓝，深度超过 20,000 英尺\",\n",
    "        Data(people=[]),\n",
    "    ),\n",
    "    (\n",
    "        \"张小三从法国远道而来，前往西班牙。\",\n",
    "        Data(people=[Person(name=\"张小三\", height_in_meters=None, hair_color=None)]),\n",
    "    ),\n",
    "]\n",
    "\n",
    "\n",
    "messages = []\n",
    "\n",
    "for txt, tool_call in examples:\n",
    "    if tool_call.people:\n",
    "        # This final message is optional for some providers\n",
    "        ai_response = \"检测到的人.\"\n",
    "    else:\n",
    "        ai_response = \"未检测到人员.\"\n",
    "    messages.extend(tool_example_to_messages(txt, [tool_call], ai_response=ai_response))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e8193773-154d-4c33-8374-0cea1655818e",
   "metadata": {},
   "source": [
    "检查结果，我们看到这两个示例对生成了八条消息："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "265d2090-e81d-4fff-9a4c-1caf618a7563",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "海洋广阔而蔚蓝，深度超过 20,000 英尺\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  Data (e4732f0f-66c6-47eb-8087-59d8ddf330f6)\n",
      " Call ID: e4732f0f-66c6-47eb-8087-59d8ddf330f6\n",
      "  Args:\n",
      "    people: []\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "\n",
      "You have correctly called this tool.\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "未检测到人员.\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "张小三从法国远道而来，前往西班牙。\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  Data (c10bbcee-fa81-4362-af90-f72df8d93c7e)\n",
      " Call ID: c10bbcee-fa81-4362-af90-f72df8d93c7e\n",
      "  Args:\n",
      "    people: [{'name': '张小三', 'hair_color': None, 'height_in_meters': None}]\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "\n",
      "You have correctly called this tool.\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "检测到的人.\n"
     ]
    }
   ],
   "source": [
    "for message in messages:\n",
    "    message.pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9af03af2-1c7f-4c04-b223-5fc2bd529c22",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
